#!/usr/bin/python2.7
# -*- coding: cp1252 -*-
#
# Copyright 2012 Darkspace revelations ltd. All Rights Reserved.
# adapted from the Startrek and Spectrek BASIC versions of way back (1970' - 1980's)

"""

    coopyentity.py
    
    Class definitions for Entities for CoopyTrek
    
"""

from datetime import datetime
from google.appengine.api import channel
import time
import logging
#import os
import random
#import sys
import json
#import pickle
import math

ENTITY_DEFAULT = 0
ENTITY_VESSEL = 1
ENTITY_SHIP = 2
ENTITY_STARBASE = 3
ENTITY_PLANET = 4
ENTITY_MOON = 5
ENTITY_STAR = 6
ENTITY_BLACK_HOLE = 7
ENTITY_AI = 10
ENTITY_KLINGON = 11
ENTITY_BORG = 12
ENTITY_SPAWNER = 20
ENTITY_WRECK = 21
ENTITY_LABELS={ ENTITY_STARBASE : "Federation Starbase",
                ENTITY_PLANET : "Planet",
                ENTITY_MOON : "Moon",
                ENTITY_STAR : "Star",
                ENTITY_KLINGON : "Kling-on",
                ENTITY_BORG: "Bail"}

ENTITY_KILLSCORES = { ENTITY_STARBASE : 5000,
                      ENTITY_KLINGON : 500,
                      ENTITY_BORG: 1000,
                      ENTITY_SHIP: 1000}

SCORE_KILL = 1
SCORE_ASSIST = 2
SCORETYPE_DAMAGE = 1

AI_RANGE_SENSE = 10     # distance before ai vessel senses ships
AI_RANGE_ATTACK = 4     # distance within which ai vessel can attack
AI_STATE_PURSUE = 2     # state: ai pursues target
AI_STATE_ATTACK = 3     # state: ai attacks target

def entityCreator (etype, game, **kwargs):
    """ Create and return an entity subclass of type etype, with its own class init()"""
    initiators = {  ENTITY_DEFAULT : None,
                    ENTITY_VESSEL : None,
                    ENTITY_SHIP : Ship,
                    ENTITY_STARBASE : Starbase,
                    ENTITY_PLANET : Celestial,
                    ENTITY_MOON : Celestial,
                    ENTITY_STAR : Celestial,
                    ENTITY_BLACK_HOLE : Celestial,
                    ENTITY_AI : None,
                    ENTITY_KLINGON : Enemy, 
                    ENTITY_BORG : Enemy,
                    ENTITY_SPAWNER : Spawner,
                    ENTITY_WRECK : Ship }
    logging.debug("Entity create: " + str(etype))
    for name,value in kwargs.items():
        logging.debug (name + "=" + str(value))
    kwargs.update({"etype":etype,"game" : game})
    if (initiators[etype]):
        return initiators[etype](**kwargs)
    else:
        logging.WARNING ("Illegal init request of entity type")
        return None
        
def send_alert(msg, user_id, game_key):
    update = {'alertmsg' : msg}
    s = json.dumps(update)
    channel.send_message(user_id + game_key, s)


class Entity():

    def __init__(self, game, etype=0, hullmax=100, shieldmax=200, scanMessage = "", events={}, isglobal=False, shield_absorption_factor = 0.5, radius = 0.5, damage_factor=1.0):
        self.key = game.assign_entity_key()
        self.etype = etype
        self.user_id = None
        self.game = game
        self.qx = 0
        self.qy = 0
        self.x = 0
        self.y = 0
        self.angle = 0
        self.radius = radius
        self.damagers={}
        self.destroyer = None
        self.repair = None
        self.damage_factor = damage_factor
        self.hull = self.hullmax = hullmax
        self.shield = self.shieldmax = shieldmax
        self.shield_absorption_factor = shield_absorption_factor
        self.range_phaser = 6
        self.photon = 0
        self.photonmax = 10
        self.range_photon = 15
        self.score = 0
        self.killscore = ENTITY_KILLSCORES.get(etype, 0)
        self.oqx = -1
        self.oqy = -1
        self.ox = 0
        self.oy = 0
        self.oangle = 0
        self.command = None
        self.command_locked = False
        self.command_speedfactor = 1.0
        self.command_startfrom = 0 #msec
        self.command_duration = 300 #msec
        self.end = 0
        self.endmsg = ""
        self.scanMessage = scanMessage
        self.alive = True
        self.force_resend_entities = False # ensure that the entities are sent on next update
        self.inspace = True # consider setting to false and 'launch' at some point
        self.mezzo = None
        self.isglobal = isglobal
        self.events = events
        
        self.animation = []
        if (etype in [ENTITY_AI, ENTITY_KLINGON, ENTITY_BORG]):
            self.has_ai = True
            self.ai_state = None
            self.ai_target = None
            self.command_startfrom = 300
            self.damage_factor = 0.5
        else:
            self.has_ai = False

    def damage_p(self):
        return int((1.0-self.hull/self.hullmax)*100)
    
    def shield_p(self):
        return int((1.0-self.shield/self.shieldmax)*100)
        
    def _event_(self,event):
    	logging.debug("Event: "+event+", " + str(self.events))
        for e in self.events.get(event,[]):
            logging.debug("Handle event: "+str(e))
            self.game.mission.eventHandler(self,e)    

    def _destroy_(self):
        self.hull = 0
        self.has_ai = False
        for ent, score in self.damagers.iteritems():
            ent.addscore(score,score_type=1,score_source=self.etype) # SCORE_DAMAGE
        if self.destroyer:
            self.destroyer[0].addscore(self.destroyer[1], score_type=2, score_source=self.etype) # SCORE_KILL
        self.damagers={}
        self.destroyer=None
        self._event_("OnDestroy")
        self.etype = -1
        self.alive = False
        
    def _hulldamage_(self, dam, from_entity):
        elabel = from_entity.elabel()
        logging.debug ("Hulldamage: "+str(dam) + "/" + str(self.hull) + " from " + elabel)
        self.hull -= dam
        if (self.hull<=0):
            self.endmsg = "You were destroyed by "+from_entity.elabel()+"."
            self.end = 2
            d = self.damagers.get(from_entity,0)
            self.destroyer = (from_entity, self.killscore-d)
        else:
            d = self.damagers.get(from_entity,0)
            self.damagers.update({from_entity: d+dam})

    def _hullrepair_(self, dam):
        self.hull += dam
        if (self.hull>=self.hullmax):
            self.damagers={}
            self.hull = self.hullmax
            self.repair = 0

    def take_damage(self, dam, from_entity):
        """ reduce damage by shield, if it is on """
        elabel = from_entity.elabel()
        logging.debug ("Damage: "+str(dam) + " from " + elabel)
        dam_shield = int(dam*self.shield_absorption_factor)
        dam_hull = dam-dam_shield
        if (dam_shield>self.shield):
            dam_hull += dam_shield-self.shield
            self.shield = 0
        else:
            self.shield -= dam_shield
        self._hulldamage_(dam_hull, from_entity)
        self.animation.append({ "key" : self.key,
                               "animationtype" : "damaged",
                               "damage" : self.damage_p(), 
                               "delay" : 1000})

    def addscore(self, score, score_type, score_source):
        self.score += score
        #self.scoretypes[score_type] += 1
        #if scoretype is kill, register the etype
        #
        #
        if self.user_id:
            send_alert("+"+str(score),self.user_id, self.game.game_key)

    def collision(self, qx,qy, x, y):
        """ returns true if x,y in rect of entity  """
        if qx==self.qx and qy==self.qy:
            x = x+0.5
            y = y+0.5
            return (abs(x-(self.x+self.radius))<self.radius) and (abs(y-(self.y+self.radius))<self.radius)
        else:
            return False
      
    def setoldloc(self):
        """ remember the current location """
        self.oqx = self.qx
        self.oqy = self.qy
        self.ox = self.x
        self.oy = self.y
        self.oangle = self.angle    

    def pre_round_update(self):
        self.setoldloc()

    def end_round_update(self):
        """ update status of the entity at end of round """
        if not(self.alive):
            return
        if (self.shield>0):
            self.shield -= min(10,self.shield)
        if (self.repair==1) or self.etype in [ENTITY_KLINGON, ENTITY_BORG]:
            self._hullrepair_(10)
        if (self.end>0):
            self.animation.append({ "key" : self.key,
                               "animationtype" : "discard",
                               "delay" : 2000})
            self._destroy_()

    def end_round_update_globals(self):
        pass

    def elabel(self):
        if (self.etype==ENTITY_SHIP):
            return self.rank+" "+self.commander
        else:
            return ENTITY_LABELS.get(self.etype, "")

    def jsonable(self):

        return {"key" : self.key,
                "etype" : self.etype,
                "radius" : self.radius,
                "shield" : self.shield,
                "qx" : self.qx,
                "qy" : self.qy,
                "x" : self.x,
                "y" : self.y,
                "angle" : self.angle,
                "damage" : self.damage_p(),
                "elabel" : self.elabel()}


class Vessel(Entity):

    def __init__(self, game, etype, energymax=15000, **kwargs):
        Entity.__init__(self, game, etype, **kwargs)
        self.energy = self.energymax = energymax


    def travel(self, game, dq, ds):
        """ vessel travels dq quadrants and ds sectors (vectors) """
        user_id = None
        msg = "" 
        if (self.etype == ENTITY_SHIP):
            user_id = self.user_id
            msg = "Travelling"
        elif (self.etype in [ENTITY_KLINGON, ENTITY_BORG]):
            if self.ai_target:
                user_id = self.ai_target.user_id
                msg = "Enemy travelling "+str(ds)        
        if user_id:
            send_alert(msg, user_id, game.game_key)

        newloc = game.new_location((self.qx,self.qy), (self.x, self.y), dq,ds)
        if newloc:
            (ingalaxy, newq, (ndqx, ndqy), (ndx,ndy), (ex,ey)) = newloc
        else:
            return
        self.x += ndx
        self.y += ndy
        self.qx += ndqx
        self.qy += ndqy
        (dx,dy) = ds 
        self.angle = int(math.atan2(float(dy),float(dx))*180/math.pi) 
        if newq:
            self.newquadrant(game)
        else:
            self.animation.append({ "key" : self.key,
                               "animationtype" : "move",
                               "etype" : self.etype,
                               "x" : self.x,
                               "y" : self.y,
                               "oangle" : self.oangle,
                               "angle" : self.angle,
                               "radius" : self.radius,
                               "shield" : self.shield,
                               "delay" : self.command_startfrom,
                               "duration" : self.command_duration})
                                     
        collision_entities = game.quadrant_collision(self.qx, self.qy, self.x,self.y)
        collision_entity_types = []
        for entity in collision_entities:
            collision_entity_types.append(entity.etype)
        if (ENTITY_PLANET in collision_entity_types):
                self.end = 7
                self.endmsg = "The ship was uncontrollably lost in the gravity of a planet in quadrant " + str(self.qx) + ",  " + str(self.qy) + " and evaporated in the planet's atmosphere."
                self.hull = 0
        elif (ENTITY_STAR in collision_entity_types):
                self.end = 8
                self.endmsg = "The ship burned up in the vicinity of a star in quadrant " + str(self.qx) + ",  " + str(self.qy) + "."
                self.hull = 0
        elif (ENTITY_MOON in collision_entity_types):
                self.end = 9
                self.endmsg = "The ship collided with a moon in quadrant " + str(self.qx) + ",  " + str(self.qy) + " and was destroyed."
                self.hull = 0

    def newquadrant(self, game):
        """ register a vessel move to new quadrant """
        try:
            send_alert("Entering new quadrant: "+str(self.qx)+", "+str(self.qy), self.user_id, game.game_key)
            game.resend_entities = True
        except:
            pass

    def entered_new_quadrant(self):
        return (self.oqx<>self.qx) or (self.oqy<>self.qy)
    
    
class Ship(Vessel):
    
    def __init__(self, game, 
            etype=ENTITY_SHIP, 
            user= None, 
            commander="Incognito", 
            rank="spacecadet", 
            photon=10, 
            **kwargs):
        Vessel.__init__(self, game, etype, **kwargs)        
        self.user = user
        if user: 
           self.user_id = user.user_id()
        else:
           self.user_id = None
        self.commander = commander
        self.rank = rank        
        self.photon = photon

    def update_commands (self, aq):
        """ check which optional commands are available, fallback sequence
            slot #3 slot #4
            PHOTON  PHASER  alerted mode
            DOCK    SCAN    near station / near celestial
            REPAIR  CHARGE  nothing / near sun
            REPAIR  PROBE   nothing / nothing
        """

        if (aq.has_entity( [ENTITY_KLINGON,ENTITY_BORG] )):
            self.command_updates = [{"slot" : 3, "code" : "PHA", "console" : "PHASER", "force" : 50},
                                   {"slot" : 4, "code" : "PHO", "console" : "PHOTON", "force" : 1}]
        else:
            self.command_updates = []
            if (aq.in_range( (self.x, self.y), 2, [ENTITY_STARBASE])):
                self.command_updates.append({"slot" : 3, "code" : "DCK", "console" : "DOCK", "force" : 1})
            else:
                self.command_updates.append({"slot" : 3, "code" : "RPR", "console" : "REPAIR", "force" : 20})
            if (aq.in_range( (self.x, self.y), 2, [ENTITY_MOON, ENTITY_PLANET])):
                self.command_updates.append({"slot" : 4, "code" : "SCN", "console" : "SCAN", "force" : 1})
            elif (aq.in_range( (self.x, self.y), 3, [ENTITY_STAR])):
                self.command_updates.append({"slot" : 4, "code" : "CRG", "console" : "CHARGE", "force" : 20})
            else:
                self.command_updates.append({"slot" : 4, "code" : "PRB", "console" : "PROBE", "force" : 20})    


class AI(Vessel):
    
    def __init__(self, **kwargs):
        Vessel.__init__(self, **kwargs)

    def ai(self, game): # klingon AI for now
        if not(self.ai_state): # idle state
            (closest_ship,dist) = game.aq.get_closest(self, [ENTITY_SHIP])
            (closest_friend,fdist) = game.aq.get_closest(self, [ENTITY_KLINGON])
            if closest_ship:
                if (dist<=AI_RANGE_SENSE):
                    send_alert("Klingon targets you!", closest_ship.user_id, game.game_key)
                    self.ai_target = closest_ship
                    self._event_("OnTarget")
                    if (dist<=AI_RANGE_ATTACK):
                        self.ai_state = AI_STATE_ATTACK
                    else:
                        self.ai_state = AI_STATE_PURSUE
            if closest_friend and not(self.ai_state): # swarm to closest friend
                if (fdist<=AI_RANGE_SENSE):
                    self.travel(game,
                            (0,0),
                            (int(((self.x+closest_friend.x)/2-self.x)+random.randint(-2,2)),
                             int(((self.y+closest_friend.y)/2-self.y)+random.randint(-2,2)))) # overshoot
            else:
                # float back to middle of quadrant
                dx = 0
                dy = 0
                if ((game.quadrant_mx-self.x)<5 or (self.x<5)):
                    dx = int(((self.x+game.quadrant_mx/2)/2-self.x)/random.randint(1,4))
                if ((game.quadrant_my-self.y)<5 or (self.y<5)):
                    dy = int(((self.x+game.quadrant_mx/2)/2-self.x)/random.randint(1,4))
                if (dx or dy):
                    self.travel(game,(0,0),(dx,dy))
        elif (self.ai_state == AI_STATE_PURSUE): # pursueing target
           sdist = math.hypot(self.x - self.ai_target.x,self.y - self.ai_target.y)
           if ((self.qx<>self.ai_target.qx) or (self.qy<>self.ai_target.qy) or (sdist>AI_RANGE_SENSE)):
               # target lost (so you can flee by leaving quadrant for now)
               self.ai_state = None
               self.ai_target = None
           elif (sdist<=AI_RANGE_ATTACK):
                self.ai_state = AI_STATE_ATTACK
           else:
                self.travel(game,
                            (0,0),
                            (int((self.x+self.ai_target.x)/2-self.x),
                             int((self.y+self.ai_target.y)/2-self.y))) # we go to the middle
        elif (self.ai_state == AI_STATE_ATTACK):
            sdist = math.hypot(self.x - self.ai_target.x,self.y - self.ai_target.y)
            if (sdist>AI_RANGE_ATTACK):
                self.ai_state = AI_STATE_PURSUE
            else:
                send_alert("Klingon attacks you!", self.ai_target.user_id, game.game_key)
                damage = int((5-sdist)*random.randint(5,10)*self.damage_factor)
                self.ai_target.take_damage(damage, self)


class Enemy(AI):
    
    def __init__(self, **kwargs):
        AI.__init__(self, **kwargs)


class Starbase(Entity):

    def __init__(self, name="", **kwargs):
        Entity.__init__(self, **kwargs)
        self.name = name
        
    def update_commands (self, aq):
        """ check which optional commands are available, fallback sequence
            slot #3 slot #4
            PHOTON  PHASER  alerted mode
            DOCK    SCAN    near station / near celestial
            REPAIR  CHARGE  nothing / near sun
            REPAIR  PROBE   nothing / nothing
        """

        if (aq.has_entity( [ENTITY_KLINGON,ENTITY_BORG] )):
            self.command_updates = [{"slot" : 3, "code" : "PHA", "console" : "PHASER", "force" : 50},
                                   {"slot" : 4, "code" : "PHO", "console" : "PHOTON", "force" : 1}]
        else:
            self.command_updates = []
            if (aq.in_range( (self.x, self.y), 2, [ENTITY_STARBASE])):
                self.command_updates.append({"slot" : 3, "code" : "DCK", "console" : "DOCK", "force" : 1})
            else:
                self.command_updates.append({"slot" : 3, "code" : "RPR", "console" : "REPAIR", "force" : 20})
            if (aq.in_range( (self.x, self.y), 2, [ENTITY_MOON, ENTITY_PLANET])):
                self.command_updates.append({"slot" : 4, "code" : "SCN", "console" : "SCAN", "force" : 1})
            elif (aq.in_range( (self.x, self.y), 3, [ENTITY_STAR])):
                self.command_updates.append({"slot" : 4, "code" : "CRG", "console" : "CHARGE", "force" : 20})
            else:
                self.command_updates.append({"slot" : 4, "code" : "PRB", "console" : "PROBE", "force" : 20})


class Celestial(Entity):

    def __init__(self, name="", scanMessage="", **kwargs):
        Entity.__init__(self, **kwargs)
        self.scanMessage = scanMessage
        self.name=name


class Spawner(Entity):

    def __init__(self, active=False, spawnEtype=0, spawnNumber=0, spawnRange=0, spawnCounter = 3, spawnKwargs = None, **kwargs):
        Entity.__init__(self, **kwargs)
        self.active = active
        self.spawnEtype = spawnEtype
        self.spawnNumber = spawnNumber
        self.spawnRange = spawnRange
        self.spawnKwargs = spawnKwargs
        self.spawnCounter = spawnCounter
        self.spawnCounterReset = spawnCounter

    def spawn(self):        
        if (self.spawnNumber>0):
            self.spawnNumber -= 1
            e = entityCreator(self.spawnEtype, self.game, **self.spawnKwargs)
            if (e):
                e.qx = self.qx
                e.qy = self.qy
                e.x = self.x + random.randint(-self.spawnRange,+self.spawnRange)
                e.y = self.y + random.randint(-self.spawnRange,+self.spawnRange)
                self.game.add_entity(e)
                self.game.resend_entities = True

    def end_round_update_globals(self):
        if self.active:
            self.spawnCounter -= 1
            if (self.spawnCounter<=0):
                self.spawn()
                self.spawnCounter = self.spawnCounterReset
            if (self.spawnNumber<=0):
                self.active = False
